# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0

# DeepSpeed Team
import torch
import torch.nn.functional as F
import time
import deepspeed
from deepspeed.runtime.zero.partition_parameters import ZeroParamStatus
from deepspeed.accelerator import get_accelerator
from dschat.rlhf.redistribution import (get_attention_distribution, get_generator_attention_distribution)
from dschat.utils.utils import print_rank_0


def print_all_ranks(tag, value, rank):
    world_size = torch.distributed.get_world_size()
    all_tensor = torch.zeros(world_size, dtype=torch.float32).to(
        get_accelerator().current_device_name())
    all_tensor[rank] = value
    torch.distributed.all_reduce(all_tensor, op=torch.distributed.ReduceOp.SUM)
    print_rank_0(f'{tag} {all_tensor}', rank)


def get_model_norm(model):
    with torch.no_grad():
        total = 0.0
        for param in model.parameters():
            should_gather = hasattr(
                param,
                'ds_id') and param.ds_status == ZeroParamStatus.NOT_AVAILABLE
            with deepspeed.zero.GatheredParameters(param,
                                                   enabled=should_gather):
                total += float(param.float().norm())

    return total


def gather_log_probs(logits, labels):
    log_probs = F.log_softmax(logits, dim=-1)
    log_probs_labels = log_probs.gather(dim=-1, index=labels.unsqueeze(-1))
    return log_probs_labels.squeeze(-1)


class DeepSpeedPPOTrainer():

    def __init__(self, rlhf_engine, args):
        self.rlhf_engine = rlhf_engine
        self.actor_model = self.rlhf_engine.actor
        self.critic_model = self.rlhf_engine.critic
        self.ref_model = self.rlhf_engine.ref
        self.reward_model = self.rlhf_engine.reward
        self.tokenizer = self.rlhf_engine.tokenizer
        self.args = args
        self.max_answer_seq_len = args.max_answer_seq_len
        self.end_of_conversation_token_id = self.tokenizer(
            args.end_of_conversation_token)['input_ids'][-1]
        self.z3_enabled = args.actor_zero_stage == 3
        self.compute_fp32_loss = self.args.compute_fp32_loss

        # In case the generated experience is not valid (too short), we use the last valid
        # generated experience. Alternatively, we can skip the step (on all workers).
        # For now, use the last valid experience which is a simpler solution
        self.last_generated_experience = None

        # Those value can be changed
        self.kl_ctl = 0.1
        self.clip_reward_value = 5
        self.cliprange = 0.2
        self.cliprange_value = 0.2
        self.gamma = 1.0
        self.lam = 0.95
        self.generate_time = 0.0

    def _generate_sequence(self, prompts, mask, step):

        # max_min_length = self.max_answer_seq_len + prompts.shape[1]
        max_min_length = self.max_answer_seq_len

        # This has been added due to a probability/nan error that happens after
        # meta-llama/Llama-2-7b-hf enabled do_sample:
        # https://huggingface.co/meta-llama/Llama-2-7b-hf/commit/6fdf2e60f86ff2481f2241aaee459f85b5b0bbb9
        if self.actor_model.module.config.model_type == "llama":
            kwargs = dict(do_sample=False)
        else:
            kwargs = dict()

        with torch.no_grad():
            seq = self.actor_model.module.generate(
                prompts,
                attention_mask=mask,
                max_new_tokens=max_min_length,
                pad_token_id=self.tokenizer.pad_token_id,
                # synced_gpus=self.z3_enabled,
                **kwargs)

        # Filter out seq with no answers (or very short). This happens when users directly use the pre-training ckpt without supervised finetuning
        # NOTE: this will causes each GPU has different number of examples
        batch_size = seq.shape[0]
        prompt_length = prompts.shape[1]
        self.prompt_length = prompt_length
        ans = seq[:, prompt_length:]
        valid_ans_len = (ans != self.tokenizer.pad_token_id).sum(dim=-1)

        if self.args.print_answers and (step % self.args.print_answers_interval
                                        == 0):
            print(
                f"--- prompt --> step={step}, rank={torch.distributed.get_rank()}, {self.tokenizer.batch_decode(prompts, skip_special_tokens=True)}"
            )
            print(
                f"--- ans    --> step={step}, rank={torch.distributed.get_rank()}, {self.tokenizer.batch_decode(ans, skip_special_tokens=True)}"
            )

        out_seq = []
        for i in range(batch_size):
            if valid_ans_len[
                    i] <= 1:  # if the answer is shorter than 1 token, drop it
                print(
                    f'Dropping too short generated answer: {step=}: \n'
                    f'prompts: {self.tokenizer.batch_decode(prompts, skip_special_tokens=False)}\n'
                    f'answers: {self.tokenizer.batch_decode(ans, skip_special_tokens=False)}'
                )
                continue
            else:
                out_seq.append(seq[i:i + 1])

        if not out_seq:
            print(
                f'All generated results are too short for rank={self.args.local_rank} step={step}\n'
                f'-> prompts: {self.tokenizer.batch_decode(prompts, skip_special_tokens=False)}\n'
                f'-> answers: {self.tokenizer.batch_decode(ans, skip_special_tokens=False)}'
            )
            return None

        out_seq = torch.cat(out_seq, dim=0)  # concat output in the batch dim

        return out_seq

    def generate_experience(self, prompts, mask, step):
        self.eval()
        generate_start = time.time()
        seq = self._generate_sequence(prompts, mask, step)
        generate_end = time.time()
        if seq is None:
            assert self.last_generated_experience is not None, f'Invalid generated experience at {step=}'
            prompts = self.last_generated_experience['prompts']
            seq = self.last_generated_experience['seq']
        else:
            self.last_generated_experience = {'prompts': prompts, 'seq': seq}
        self.train()

        pad_token_id = self.tokenizer.pad_token_id
        attention_mask = seq.not_equal(pad_token_id).long()
        reward_list = []
        abc_reward_list = []
        uniform_reward_list = []
        with torch.no_grad():
            output = self.actor_model(seq, attention_mask=attention_mask)
            output_ref = self.ref_model(seq, attention_mask=attention_mask)
            # FC altered
            # my method
            res = self.reward_model.forward_value(
                seq, attention_mask,
                prompt_length=self.prompt_length)
            reward_score = res['chosen_end_scores'].detach(
            )
            ends_pos = res['ends_pos']
            rm_values = res['values'].detach()
            ans_lengths = []  # 为了给abc方法计算用于奖励的数组长度
            if len(ends_pos) != 0:
                for i in range(rm_values.shape[0]):
                    reward_list.append(rm_values[i, prompts.shape[1] - 1:ends_pos[i]])
                    ans_lengths.append(reward_list[i].shape[0])
                    # print("reward_list[i]_before", rm_values[i, prompts.shape[1]+1:ends_pos[i]])
            # my method end
            # ABC method
            beta = 0.5
            attentions = res['attentions']
            attentions_mean = attentions[-1].mean(1)
            for index in range(seq.shape[0]):
                response = seq[index][:prompts.shape[1]]
                query = prompts[index]
                attention = attentions_mean[index]
                reward = torch.zeros_like(response, dtype=float)
                total = reward_score[index]
                reward[-1] = (1 - beta) * total
                redist_reward = (
                        torch.tensor(
                            get_attention_distribution(response, query, attention),
                            device=reward.device,
                        )
                        * total
                        * beta
                )
                reward += redist_reward
                reward = reward[reward.shape[0] - ans_lengths[index]:]
                # print("total", total)
                # print("reward", reward)
                # print("reward_size", reward.size())
                abc_reward_list.append(reward)
                # ABC end
                # uniform method
                reward_uniform = torch.zeros_like(reward, dtype=float)
                reward_uniform += total / reward.shape[0]
                uniform_reward_list.append(reward_uniform)
            # uniform end
            #     print("end_pos[i]", ends_pos[i])
            #     print("rm_values[0] except prompt", rm_values[i, prompts.shape[1]+1:ends_pos[i]])
            # end
            # FC writed
            # 第一个token作为state，不看作action，所以加1
            # sub_seq_list, sub_seq_mask_list = generate_sub_tensors_with_mask_2d(seq, prompts.shape[1] + 1)
            # for i in range(len(sub_seq_list)):
            #     sub_seq_reward = []
            #     chunks_seq = chunk_tensor_by_8_rows(sub_seq_list[i])
            #     chunks_mask = chunk_tensor_by_8_rows(sub_seq_mask_list[i])
            #     for j in range(len(chunks_seq)):
            #         sub_reward_score = self.reward_model.forward_value(
            #             chunks_seq[j], chunks_mask[j],
            #             prompt_length=self.prompt_length)['chosen_end_scores'].detach(
            #         )
            #         sub_seq_reward.append(sub_reward_score)
            #     one_seq_reward = torch.cat(sub_seq_reward)
            #     reward_list.append(one_seq_reward)
            #     print("reward_list[i]", reward_list[i])
            # end
            values = self.critic_model.forward_value(
                seq, attention_mask, return_value_only=True).detach()[:, :-1]
        #
        #
        #
        logits = output.logits
        logits_ref = output_ref.logits
        if self.compute_fp32_loss:
            logits = logits.to(torch.float)
            logits_ref = logits_ref.to(torch.float)

        self.generate_time = generate_end - generate_start
        p = F.softmax(logits[:, :-1, :], dim=-1)  # 当前策略的概率分布，shape: [B, T, V]
        log_p = F.log_softmax(logits[:, :-1, :], dim=-1)  # 当前策略的 log 概率
        log_q = F.log_softmax(logits_ref[:, :-1, :], dim=-1)  # 参考策略的 log 概率

        # 计算每个 token 的 KL 散度：对词汇表的维度求和
        per_token_kl = torch.sum(p * (log_p - log_q), dim=-1)  # shape: [B, T]

        # 如果有 action_mask，用来筛选哪些 token 的 KL 要计入损失
        sum_kl = (per_token_kl * attention_mask[:, 1:]).sum(dim=1)  # shape: [B]
        print("sum_kl", sum_kl)
        return {
            'prompts': prompts,
            'logprobs': gather_log_probs(logits[:, :-1, :], seq[:, 1:]),
            'ref_logprobs': gather_log_probs(logits_ref[:, :-1, :], seq[:,
                                                                    1:]),
            'value': values,
            'rewards': reward_score,
            'input_ids': seq,
            "attention_mask": attention_mask,
            "new_reward_score": reward_list,
            "abc_reward_score": abc_reward_list,
            "uniform_reward_score": uniform_reward_list
        }

    def compute_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                        action_mask):
        print("reward_score", reward_score)
        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        rewards = kl_divergence_estimate
        start = prompts.shape[1] - 1
        ends = start + action_mask[:, start:].sum(1) + 1
        reward_clip = torch.clamp(reward_score, -self.clip_reward_value,
                                  self.clip_reward_value)
        batch_size = log_probs.shape[0]
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j]

        return rewards

    def compute_ABC_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                            action_mask, abc_reward_score):

        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        print("reward_score", reward_score)
        rewards = kl_divergence_estimate
        start = prompts.shape[1] - 1
        ends = start + action_mask[:, start:].sum(1) + 1
        batch_size = log_probs.shape[0]
        reward_score_backup = torch.zeros_like(reward_score)
        for j in range(batch_size):
            reward_score_backup[j] = abc_reward_score[j][-1]
            abc_reward_score[j][-1] = 0
        reward_clip = torch.clamp(reward_score_backup, -self.clip_reward_value,
                                  self.clip_reward_value)
        # print(reward_clip)
        print("method--abc")
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j]
            # 加2是因为start是prompt的最后一个token的位置，而奖励是从ans的第二个token开始计算的
            # 如果end超出了reward的列数，那么从+1开始
            # print("data[j]", data[j])
            if ends[j] > rewards.shape[1]:
                rewards[j, start + 1:ends[j]] += abc_reward_score[j]
                # print("final_reward", rewards[j, start+1:ends[j]])
            else:
                rewards[j, start:ends[j]] += abc_reward_score[j]
                # print("final_reward", rewards[j, start+2:ends[j]])
        return rewards

    def compute_Uniform_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                                action_mask, uniform_reward_list):

        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        rewards = kl_divergence_estimate
        start = prompts.shape[1] - 1
        ends = start + action_mask[:, start:].sum(1) + 1
        batch_size = log_probs.shape[0]
        reward_score_backup = torch.zeros_like(reward_score)
        for j in range(batch_size):
            reward_score_backup[j] = uniform_reward_list[j][-1]
            uniform_reward_list[j][-1] = 0
        reward_clip = torch.clamp(reward_score_backup, -self.clip_reward_value,
                                  self.clip_reward_value)
        print("method--uniform")
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j]
            # 加2是因为start是prompt的最后一个token的位置，而奖励是从ans的第二个token开始计算的
            # 如果end超出了reward的列数，那么从+1开始
            # print("data[j]", data[j])
            if ends[j] > rewards.shape[1]:
                rewards[j, start + 1:ends[j]] += uniform_reward_list[j]
                # print("final_reward", rewards[j, start+1:ends[j]])
            else:
                rewards[j, start :ends[j]] += uniform_reward_list[j]
                # print("final_reward", rewards[j, start+2:ends[j]])
        return rewards

    def new_compute_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                            action_mask, new_reward_score, beta=0.3, scope=10):
        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        rewards = kl_divergence_estimate
        # print("sum_kl", sum_kl)
        print("rewards_score", reward_score)
        start = prompts.shape[1] - 1
        ends = start + action_mask[:, start:].sum(1) + 1
        reward_clip = torch.clamp(reward_score * (1 - beta), -self.clip_reward_value,
                                  self.clip_reward_value)
        print("method--mymethod")
        batch_size = log_probs.shape[0]
        data = new_reward_score
        # print("len(data)", len(data))
        # print("reward_score.shape", reward_score.shape)
        tensor = [torch.zeros_like(tensor) for tensor in data]
        for j in range(len(data)):
            # 第一个位置固定为 0
            row = data[j].clone()
            # row[0] = 0
            for m in range(len(row)):
                row[m] = 0 if row[m] < 0 else row[m]
                row[m] = row[-1] if row[m] >= row[-1] else row[m]
            # 从后往前遍历，保证 “diff = row[i] - row[i-1]” 中的 row[i-1] 还是原值
            for i in range(len(row) - 1, 0, -1): #如果我把最后一个的diff加上去呢?
                diff = (row[i] - row[i - 1])
                # if diff < 0: diff = 0 #保证不对一般的token进行惩罚
                # for n in range(scope):
                for n in range(1, scope):
                    if (i - n < -1): continue
                    # tensor[j][i - n] += diff * math.pow(beta, n)
                    tensor[j][i + 1 - n] += diff * math.pow(beta, n)
            #     tensor[j][i - 1] += diff * 0.4
            #     if i>=2: tensor[j][i - 2] += diff * 0.4 * 0.4
            #     if i>=3: tensor[j][i - 3] += diff * 0.4 * 0.4 * 0.4
            # tensor[j][0] = 0
            # tensor[j][1] = 0
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j]
            # 加2是因为start是prompt的最后一个token的位置，而奖励是从ans的第二个token开始计算的
            # 如果end超出了reward的列数，那么从+1开始
            # print("data[j]", data[j])
            if ends[j] > rewards.shape[1]:
                rewards[j, start + 1:ends[j]] += uniform_reward_list[j]
                # print("final_reward", rewards[j, start+1:ends[j]])
            else:
                rewards[j, start :ends[j]] += uniform_reward_list[j]

        return rewards

    def train_rlhf(self, inputs):
        # train the rlhf mode here
        ### process the old outputs
        prompts = inputs['prompts']
        log_probs = inputs['logprobs']
        ref_log_probs = inputs['ref_logprobs']
        reward_score = inputs['rewards']
        values = inputs['value']
        attention_mask = inputs['attention_mask']
        seq = inputs['input_ids']
        new_reward_score = inputs['new_reward_score']
        abc_reward_score = inputs['abc_reward_score']
        uniform_reward_score = inputs['uniform_reward_score']

        start = prompts.size()[-1] - 1
        action_mask = attention_mask[:, 1:]

        old_values = values
        # print("actor_model", self.actor_model)
        # print("actor_model module", self.actor_model.module)
        with torch.no_grad():
            # old_rewards = self.compute_rewards(prompts, log_probs,
            #                                        ref_log_probs, reward_score,
            #                                        action_mask)
            # old_rewards = self.new_compute_rewards(prompts, log_probs,
            #                                        ref_log_probs, reward_score,
            #                                        action_mask, new_reward_score)
            # old_rewards = self.new_compute_rewards_beta(prompts, log_probs,
            #                                        ref_log_probs, reward_score,
            #                                        action_mask, new_reward_score)
            # old_rewards = self.new_compute_rewards2(prompts, log_probs,
            #                                        ref_log_probs, reward_score,
            #                                        action_mask, new_reward_score)
            # old_rewards = self.new_compute_rewards3(prompts, log_probs,
            #                                        ref_log_probs, reward_score,
            #                                        action_mask, new_reward_score)
            old_rewards = self.compute_ABC_rewards(prompts, log_probs,
                                                ref_log_probs, reward_score,
                                                action_mask, abc_reward_score)
            # old_rewards = self.compute_Uniform_rewards(prompts, log_probs,
            #                                       ref_log_probs, reward_score,
            #                                       action_mask, uniform_reward_score)
            ends = start + action_mask[:, start:].sum(1) + 1
            # print("old_rewards 设置为0")
            # old_rewards = old_rewards.zero_()
            # we need to zero out the reward and value after the end of the conversation
            # otherwise the advantage/return will be wrong
            for i in range(old_rewards.shape[0]):
                old_rewards[i, ends[i]:] = 0
                old_values[i, ends[i]:] = 0
            advantages, returns = self.get_advantages_and_returns(
                old_values, old_rewards, start)
        # advantages = advantages.zero_()
        ### process the new outputs
        batch = {'input_ids': seq, "attention_mask": attention_mask}
        actor_prob = self.actor_model(**batch, use_cache=False).logits
        actor_log_prob = gather_log_probs(actor_prob[:, :-1, :], seq[:, 1:])
        actor_loss = self.actor_loss_fn(actor_log_prob[:, start:],
                                        log_probs[:, start:], advantages,
                                        action_mask[:, start:])
        self.actor_model.backward(actor_loss)

        if not self.args.align_overflow:
            self.actor_model.step()

        value = self.critic_model.forward_value(**batch,
                                                return_value_only=True,
                                                use_cache=False)[:, :-1]
        critic_loss = self.critic_loss_fn(value[:, start:], old_values[:,
                                                                       start:],
                                          returns, action_mask[:, start:])
        self.critic_model.backward(critic_loss)

        if self.args.align_overflow:
            actor_overflow = self.actor_model.optimizer.check_overflow(
                external=True)
            critic_overflow = self.critic_model.optimizer.check_overflow(
                external=True)

            rank = torch.distributed.get_rank()
            if actor_overflow and not critic_overflow:
                self.critic_model.optimizer.skip_step = True
                print_rank_0(
                    "OVERFLOW: actor overflow, skipping both actor and critic steps",
                    rank)
            elif not actor_overflow and critic_overflow:
                self.actor_model.optimizer.skip_step = True
                print_rank_0(
                    "OVERFLOW: critic overflow, skipping both actor and critic steps",
                    rank)
            elif actor_overflow and critic_overflow:
                print_rank_0(
                    "OVERFLOW: actor and critic overflow, skipping both actor and critic steps",
                    rank)
            self.actor_model.step()

        self.critic_model.step()

        return actor_loss, critic_loss

    def get_overflow(self):
        # Overflow is not expected when using bf16
        # Therefore, DeepSpeed's BF16_Optimizer does not maintain an overflow indication
        if self.args.dtype == "bf16":
            return False, False

        actor_overflow = self.actor_model.optimizer.overflow
        critic_overflow = self.critic_model.optimizer.overflow

        return actor_overflow, critic_overflow

    def actor_loss_fn(self, logprobs, old_logprobs, advantages, mask):
        ## policy gradient loss
        log_ratio = (logprobs - old_logprobs) * mask
        ratio = torch.exp(log_ratio)
        pg_loss1 = -advantages * ratio
        pg_loss2 = -advantages * torch.clamp(ratio, 1.0 - self.cliprange,
                                             1.0 + self.cliprange)
        pg_loss = torch.sum(torch.max(pg_loss1, pg_loss2) * mask) / mask.sum()
        return pg_loss

    def critic_loss_fn(self, values, old_values, returns, mask):
        ## value loss
        values_clipped = torch.clamp(
            values,
            old_values - self.cliprange_value,
            old_values + self.cliprange_value,
        )
        if self.compute_fp32_loss:
            values = values.float()
            values_clipped = values_clipped.float()
        vf_loss1 = (values - returns)**2
        vf_loss2 = (values_clipped - returns)**2
        vf_loss = 0.5 * torch.sum(
            torch.max(vf_loss1, vf_loss2) * mask) / mask.sum()
        return vf_loss

    def get_advantages_and_returns(self, values, rewards, start):
        # Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
        lastgaelam = 0
        advantages_reversed = []
        length = rewards.size()[-1]
        for t in reversed(range(start, length)):
            nextvalues = values[:, t + 1] if t < length - 1 else 0.0
            delta = rewards[:, t] + self.gamma * nextvalues - values[:, t]
            lastgaelam = delta + self.gamma * self.lam * lastgaelam
            advantages_reversed.append(lastgaelam)
        advantages = torch.stack(advantages_reversed[::-1], dim=1)
        returns = advantages + values[:, start:]
        return advantages.detach(), returns

    def _validate_training_mode(self):
        assert self.actor_model.module.training
        assert self.critic_model.module.training

    def _validate_evaluation_mode(self):
        assert not self.actor_model.module.training
        assert not self.critic_model.module.training
        assert not self.ref_model.module.training
        assert not self.reward_model.module.training

    def train(self):
        self.actor_model.train()
        self.critic_model.train()

    def eval(self):
        self.actor_model.eval()
        self.critic_model.eval()
        self.reward_model.eval()
        self.ref_model.eval()

    def dump_model_norms(self, tag):
        actor_model_norm = get_model_norm(self.actor_model)
        ref_model_norm = get_model_norm(self.ref_model)
        critic_model_norm = get_model_norm(self.critic_model)
        reward_model_norm = get_model_norm(self.reward_model)
        print_all_ranks(f'{tag} global_actor_model_norm', actor_model_norm,
                        self.args.local_rank)
        print_all_ranks(f'{tag} global_ref_model_norm', ref_model_norm,
                        self.args.local_rank)
        print_all_ranks(f'{tag} global_critic_model_norm', critic_model_norm,
                        self.args.local_rank)
        print_all_ranks(f'{tag} global_reward_model_norm', reward_model_norm,
                        self.args.local_rank)


class DeepSpeedPPOTrainerUnsupervised(DeepSpeedPPOTrainer):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def train_unsupervised(self, inputs, unsup_coef):
        # Train the unsupervised model here
        self._validate_training_mode()

        outputs = self.actor_model(**inputs, use_cache=False)
        loss = outputs.loss
        self.actor_model.backward(unsup_coef * loss)
        self.actor_model.step()

        return loss
